----------------------------------------------------------------
-- UTILITY FUNCTIONS
----------------------------------------------------------------
local _format = string.format
local _match  = string.match
local _gmatch = string.gmatch
local _sub    = string.sub
local _find   = string.find

local _insert = table.insert
local _concat = table.concat
local _remove = table.remove

local clamp = function(x, a, b)
	if x < a then return a end
	if x > b then return b end
	return x
end

local _tb = function(x) return _floor(clamp(x, 0, 1) * 255 + 0.5) end

local colorToBytes = love.math.colorToBytes or function(r, g, b, a)
	if type(r) == "table" then
		r, g, b, a = r[1], r[2], r[3], r[4]
	end
	return _tb(r), _tb(g), _tb(b), a and _tb(a)
end

local _fb = function(x) return clamp(_floor(x + 0.5) / 255, 0, 1) end

local colorFromBytes = love.math.colorFromBytes or function(r, g, b, a)
	if type(r) == "table" then
		r, g, b, a = r[1], r[2], r[3], r[4]
	end
	return _fb(r), _fb(g), _fb(b), a and _fb(a)
end

--local default_color = function() return {1, 1, 1, 1} end

local clear_unused = function(cache)
	for k, v in pairs(cache) do
		if not v.used then cache[k] = nil end
	end
end

local invalidate = function(cache)
	for k, v in pairs(cache) do v.used = false end
end

local split = function(s, sep)
	local t = {}; local init = 1; local m, n
	sep = sep or '\n'
	while true do
		m, n = _find(s, sep, init, true)
		if m == nil then
			_insert(t, _sub(s, init))
			break
		end
		_insert(t, _sub(s, init, m - 1))
		init = n + 1
	end
	return t
end

-- returns a table repr. of normalized path: an npath
-- nparent is the npath containing path
-- nparent is either nil or already normalized
local normalize_path = function(path, nparent)
	path = split(path, "/")
	local npath = {}
	local skip = 0
	for i = #path, 1, -1 do
		local v = path[i]
		if v == ".." then skip = skip + 1
		elseif v ~= "." and v ~= "" then
			if skip > 0 then skip = skip - 1
			else _insert(npath, 1, v) end
		end
	end
	
	if nparent then
		for i = #nparent - skip, 1, -1 do
			_insert(npath, 1, nparent[i])
		end
	else
		path.skip = skip
		nparent = path
	end
	if nparent[1] == "" then _insert(npath, 1, "") end
	return npath
end

local _sx = function(s, i, j)
	return tonumber(_sub(s, i, j), 16)
end

local parse_colorbytes = function(s, require_crunch)
	if not s then return end
	local crunch, s = _match(s, '^(#?)(%x+)')
	if not s or (require_crunch and crunch ~= "#") then
		print(_format("not a color string:%s", s))
		return
	end
	
	local R, G, B, A
	if #s == 6 then -- #RRGGBB
		   R, G, B = _sx(s, 1, 2), _sx(s, 3, 4), _sx(s, 5, 6)
	elseif #s == 8 then -- #AARRGGBB
		A, R, G, B = _sx(s, 1, 2), _sx(s, 3, 4), _sx(s, 5, 6), _sx(s, 7, 8)
	else
		print(_format("unsupported color string: %s%s", crunch, s))
		return
	end
	return R, G, B, A
end

local parse_color = function(s, crunch)
	local r,g,b,a = parse_colorbytes(s, crunch)
	if r then return {colorFromBytes(r, g, b, a)} end
end


----------------------------------------------------------------
-- LOADER INIT
----------------------------------------------------------------
local Loader = {
	filter = {
		min = "nearest", -- min filter mode
		mag = "nearest", -- mag filter mode
		anisotropy = 1,  -- max anisotropy
	},
	useSpriteBatch = true,
	drawObjects    = true,
	cache = {},
}

local cache = Loader.cache

local PATH = (...):gsub("[\\/]", "."):match(".+%.") or ''
local xml = require(PATH .. "xml")

----------------------------------------------------------------
-- SECTION PARSERS
----------------------------------------------------------------
local parse_properties = function(t)
	local props = {}
	for _,v in ipairs(t) do
		if v._name == "property" then
			local attr = v._attr
			local ptype, pvalue = attr.type, attr.value
			if ptype == "bool" then
				if     pvalue == "true"  then pvalue = true
				elseif pvalue == "false" then pvalue = false
				else pvalue = nil end
			elseif ptype == "int" or ptype == "float" then
				pvalue = tonumber(pvalue)
			elseif ptype == "color" then
				if pvalue == "" then pvalue = {1, 1, 1, 1}
				else pvalue = parse_color(pvalue, true) end
			elseif ptype == "file" then
				pvalue = {
					path     = pvalue,
					filepath = _concat(normalize_path(pvalue, Loader.npath), "/"),
				}
			end
			
			props[attr.name] = pvalue
		end
	end
	return props
end

local parse_tileset = function(t)
	local attr = t._attr
	local firstgid = tonumber(attr.firstgid)
	local tsx = attr.source
	if tsx then -- external tileset
		local path = _concat(normalize_path(tsx, Loader.npath), "/")
		t = xml.parse(love.filesystem.read(path))
		for _,v in ipairs(t) do
			if v._name == "tileset" then t = v break end
		end
		attr = t._attr
		attr.firstgid = firstgid
		--attr.source   = tsx
	end

	local tileset = {
		firstgid   = firstgid,
		--source     = tsx,
		name       = attr.name,
		tileWidth  = tonumber(attr.tilewidth),
		tileHeight = tonumber(attr.tileheight),
		spacing    = tonumber(attr.spacing) or 0,
		margin     = tonumber(attr.margin)  or 0,
		tileCount  = tonumber(attr.tilecount),
		columns    = tonumber(attr.columns),
	}
	
	local props
	local tileProperties = {}
	local tileTypes = {}
	local offsetX, offsetY = 0, 0
	local cached
	
	for _, v in ipairs(t) do
		local vname, vattr = v._name, v._attr
		if vname == "image" then
			assert(cached == nil, "multiple image sections not supported")
			local source = vattr.source
			local path = _concat(normalize_path(source, Loader.npath), "/")
			cached = cache[path]
			if not cached then
				local image = love.image.newImageData(path) -- exists?
				local trans = vattr.trans
				if trans then
					local R, G, B = parse_colorbytes(trans)
					local rb, gb, bb
					image:mapPixel( function(x, y, r, g, b, a)
						rb, gb, bb = colorToBytes(r, g, b)
						if R == rb and G == gb and B == bb then return r, g, b, 0 end
						return r,g,b,a
					end)
					trans = {colorFromBytes(R, G, B)}
				end
				local filter = Loader.filter
				image = love.graphics.newImage(image)
				image:setFilter(filter.min, filter.mag, filter.anisotropy)
				cached = {image = image, trans = trans, source = source}
				cached.width, cached.height = image:getDimensions()
			end
			cached.used = true
			cache[path] = cached
		elseif vname == "tile" then
			local tilegid = firstgid + vattr.id
			if vattr.type then tileTypes[tilegid] = vattr.type end
			for _, v2 in ipairs(v) do
				if v2._name == "properties" then
					tileProperties[tilegid] = parse_properties(v2)
					break
				end
			end
		elseif vname == "properties" then
			assert(cached == nil, "multiple properties sections not supported")
			props = parse_properties(v)
		elseif vname == "tileoffset" then
			assert(offsetX == nil, "multiple tileoffset sections not supported")
			offsetX, offsetY = vattr.x, vattr.y
		end
	end

	assert(cached, "parse_tileset - tileset does not contain an image")
	
	tileset.image      = cached.image
	tileset.imageTrans = cached.trans
	tileset.imagePath  = cached.source
	tileset.width      = cached.width
	tileset.height     = cached.height
	
	tileset.offsetX    = tonumber(offsetX) or 0
	tileset.offsetY    = tonumber(offsetY) or 0
	
	tileset.properties     = props or {}
	tileset.tileProperties = tileProperties
	tileset.tileTypes      = tileTypes

	return tileset
end

local parse_tilelayer_data = function(t)
	local data = {}
	local encoding = t._attr.encoding
	
	if encoding == nil then -- xml
		for k, v in ipairs(t) do
			if v._name == "tile" then 
				_insert(data, tonumber(v._attr.gid) or 0)
			end
		end
	elseif encoding == "csv" then
		for s in _gmatch(t[1], "%d+") do
			_insert(data, tonumber(s))
		end
	elseif encoding == "base64" then
		local compfmt = t._attr.compression
		local decoded = love.data.decode("string", "base64", t[1])
		
		if compfmt == "gzip" or compfmt == "zlib"  then
			decoded = love.data.decompress("string", compfmt, decoded)
		end
		
		local pos = 1
		for i = 1, math.floor(#decoded / 4) do
			data[i], pos = love.data.unpack("<I4", decoded, pos)
		end
	else
		--data = nil
		print("parse_tilelayer_data - unsupported encoding")
	end
	
	return data
end

local parse_tilelayer = function(t)
	local attr = t._attr
	local layer = {
		id      = tonumber(attr.id),
		name    = attr.name or ("Tile Layer " .. attr.id),
		width   = tonumber(attr.width),
		height  = tonumber(attr.height),
		opacity = tonumber(attr.opacity) or 1,
		visible = tonumber(attr.visible) ~= 0,
		offsetX = tonumber(attr.offsetx) or 0,
		offsetY = tonumber(attr.offsety) or 0,
	}
	
	local data, props
	for _, v in ipairs(t) do
		local vname = v._name
		if     vname == "data" then 
			assert(data == nil, "multiple data sections not supported")
			data = parse_tilelayer_data(v) 
		elseif vname == "properties" then
			assert(props == nil, "multiple properties sections not supported")
			props = parse_properties(v)
		end
	end
	
	layer.properties = props or {}
	layer.data = data
	layer.type = "tilelayer"

	return layer
end

local parse_objectlayer = function(t)
	local attr = t._attr
	local layer = {
		id = tonumber(attr.id),
		name = attr.name or ("Object Layer " .. attr.id),
		color = parse_color(attr.color, true) or {0.5, 0.5, 0.5, 1},
		opacity = tonumber(attr.opacity) or 1,
		visible = tonumber(attr.visible) ~= 0,
		offsetX = tonumber(attr.offsetx) or 0,
		offsetY = tonumber(attr.offsety) or 0,
		drawOrder = attr.draworder or "topdown"
	}
	
	local objects = {}
	local props
	for _, v in ipairs(t) do
		if v._name == "object" then
			local vattr = v._attr
			local obj = {
				id       = tonumber(vattr.id),
				name     = vattr.name or "",
				type     = vattr.type or "",
				x        = tonumber(vattr.x),
				y        = tonumber(vattr.y),
				width    = tonumber(vattr.width)  or 0,
				height   = tonumber(vattr.height) or 0,
				rotation = tonumber(vattr.rotation) or 0,
				gid      = tonumber(vattr.gid),
				visible  = tonumber(vattr.visible) ~= 0,
			}
			_insert(objects, obj)
			for _, v2 in ipairs(v) do
				local v2name = v2._name
				if     v2name == "properties" then 
					obj.properties = parse_properties(v2)
				elseif v2name == "ellipse"  then obj.ellipse = true
				elseif v2name == "point"    then obj.point   = true
				elseif v2name == "text"     then obj.text    = v2[1] or ""
				elseif v2name == "polyline" then
					obj.polyline = {}
					for num in _gmatch(v2._attr.points, "-?%d+") do
						_insert(obj.polyline, tonumber(num))
					end
				elseif v2name == "polygon"  then
					obj.polygon = {}
					for num in _gmatch(v2._attr.points, "-?%d+") do
						_insert(obj.polygon, tonumber(num))
					end
				end
			end
		elseif v._name == "properties" then
			props = parse_properties(v)
		end
	end
	
	layer.properties = props or {}
	layer.objects = objects
	layer.type = "objectgroup"
	
	return layer
end

-- parse map
local parse_map = function(t)
	local attr = t._attr
	local map = {
		version         = attr.version,
		tiledVersion    = attr.tiledversion,
		orientation     = attr.orientation,
		renderOrder     = attr.renderorder,
		width           = tonumber(attr.width),
		height          = tonumber(attr.height),
		tileWidth       = tonumber(attr.tilewidth),
		tileHeight      = tonumber(attr.tileheight),
		hexsidelength   = tonumber(attr.hexsidelength),
		staggerAxis     = attr.staggeraxis,
		staggerIndex    = attr.staggerindex,
		backgroundColor = parse_color(attr.backgroundcolor, true),-- or {0, 0, 0, 1},
		infinite        = tonumber(attr.infinite) == 1,
		nextLayerID     = tonumber(attr.nextlayerid),
		nextObjectID    = tonumber(attr.nextobjectid),
	}
	
	local props
	local tilesets, layers = {}, {}
	for _, v in ipairs(t) do
		local vname = v._name
		if     vname == "properties" then
			assert(props == nil, "multiple properties sections not supported")
			props = parse_properties(v)
		elseif vname == "tileset" then 
			local tileset = parse_tileset(v, map)
			tilesets[tileset.name] = tileset
		elseif vname == "layer" then
			_insert(layers, parse_tilelayer(v))
		elseif vname == "objectgroup" then
			_insert(layers, parse_objectlayer(v))
		end
	end
	
	map.properties = props or {}
	map.tilesets   = tilesets
	map.layerOrder = layers
	
	return map
end

----------------------------------------------------------------
-- LOADS A TMX FILE
----------------------------------------------------------------
local Map         = require(PATH .. "Map")
local TileSet     = require(PATH .. "TileSet")
local TileLayer   = require(PATH .. "TileLayer")
local Object      = require(PATH .. "Object")
local ObjectLayer = require(PATH .. "ObjectLayer")

Loader.load = function(filepath)
	local npath = normalize_path(filepath)
	Loader.filename = _remove(npath)
	Loader.filedir  = _concat(npath, "/")
	Loader.filepath = Loader.filedir .. "/" .. Loader.filename
	Loader.npath    = npath
	
	filepath = Loader.filepath
	
	if not love.filesystem.getInfo(filepath, "file") then
		print("Loader.load - could not find the file: " .. filepath)
		return
	end
	local parsed = xml.parse(love.filesystem.read(filepath))
	
	local map
	for i, v in ipairs(parsed) do
		if v._name == "map" then
			invalidate(cache)
			map = parse_map(v)
			clear_unused(cache)
			break
		end
	end
	if  not map then
		print("Loader.load - missing map section in file: " .. filepath)
		return
	end
	
	map.directory      = Loader.filedir
	map.name           = Loader.filename
	map.useSpriteBatch = Loader.useSpriteBatch
	map.drawObjects    = Loader.drawObjects
	map.visible        = true
	
	Map.init(map)
	
	for k, v in pairs(map.tilesets) do
		v.map = map
		TileSet.init(v)
	end
	map:updateTiles()
	
	map.layers = {}
	for i, v in ipairs(map.layerOrder) do
		v.map = map
		if     v.type == "tilelayer" then
			v.useSpriteBatch = map.useSpriteBatch
			TileLayer.init(v)
			v:_populate(v.data)
			v.data = nil
		elseif v.type == "objectgroup" then
			ObjectLayer.init(v)
			for _, obj in ipairs(v.objects) do
				obj.layer = v
				Object.init(obj)
			end
		end
		map.layers[v.name] = v
		--map.layerOrder[#map.layerOrder + 1] = v
	end
	
	return map
end

----------------------------------------------------------------
-- SAVES A MAP AS TMX (REMOVED)
----------------------------------------------------------------
local xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n'
local defaultSavePath = "Saved Maps"

-- save a tmx file
function Loader.save(map, filename, path)
	path = path or defaultSavePath
	if not love.filesystem.getInfo(path, "directory") then
		love.filesystem.createDirectory(path)
	end
	--love.filesystem.write(path .. "/" .. filename,
	--	xmlHeader .. xml.toxml(write_map(map)) )
end

return Loader
